/**
* Copyright (c) 2012 to original author or authors
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package io.takari.aether.connector.test.suite;
import static org.eclipse.aether.transfer.TransferEvent.EventType.INITIATED;
import static org.eclipse.aether.transfer.TransferEvent.EventType.PROGRESSED;
import static org.eclipse.aether.transfer.TransferEvent.EventType.SUCCEEDED;
import static org.junit.Assert.assertArrayEquals;
import io.takari.aether.connector.test.suite.server.ResourceServer;
import io.tesla.webserver.WebServer;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.internal.test.util.TestFileUtils;
import org.eclipse.aether.metadata.DefaultMetadata;
import org.eclipse.aether.metadata.Metadata;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.spi.connector.ArtifactDownload;
import org.eclipse.aether.spi.connector.ArtifactUpload;
import org.eclipse.aether.spi.connector.MetadataDownload;
import org.eclipse.aether.spi.connector.MetadataUpload;
import org.eclipse.aether.spi.connector.RepositoryConnector;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.Transfer;
import org.eclipse.aether.transfer.AbstractTransferListener;
import org.eclipse.aether.transfer.NoRepositoryConnectorException;
import org.eclipse.aether.transfer.TransferCancelledException;
import org.eclipse.aether.transfer.TransferEvent;
import org.eclipse.aether.transfer.TransferEvent.EventType;
import org.eclipse.aether.transfer.TransferListener;
import org.eclipse.aether.transfer.TransferResource;
import org.junit.Assert;
import org.junit.Test;
/**
* The ConnectorTestSuite bundles standard tests for {@link RepositoryConnector}s. The only thing the client code
* must provide is a {@link RepositoryConnectorFactory}.
* <p>
* To use these tests, provide a (Junit4-)class extending this class, and provide a default constructor calling
* {@link AetherConnectorTest#ConnectorTestSuite(ConnectorTestSetup)} with a self-implemented {@link ConnectorTestSetup}.
*/
public class AetherConnectorTest extends AetherTestCase {
protected void configureServer(WebServer server) {
addBehaviour("/*", new ResourceServer());
}
/**
* Test successful event order.
*
* @see TransferEventTester#testSuccessfulTransferEvents(RepositoryConnectorFactory, TestRepositorySystemSession,
* RemoteRepository)
*/
public void testSuccessfulEvents() throws NoRepositoryConnectorException, IOException {
testSuccessfulTransferEvents(repositoryConnectorFactory, session(), remoteRepository());
}
public void testFailedEvents() throws NoRepositoryConnectorException, IOException {
testFailedTransferEvents(repositoryConnectorFactory, session(), remoteRepository());
}
public void testFileHandleLeakage() throws IOException, NoRepositoryConnectorException {
Artifact artifact = new DefaultArtifact("testGroup", "testArtifact", "", "jar", "1-test");
Metadata metadata = new DefaultMetadata("testGroup", "testArtifact", "1-test", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
RepositoryConnector connector = connector();
File tmpFile = TestFileUtils.createTempFile("testFileHandleLeakage");
ArtifactUpload artUp = new ArtifactUpload(artifact, tmpFile);
connector.put(Arrays.asList(artUp), null);
assertTrue("Leaking file handle in artifact upload", tmpFile.delete());
tmpFile = TestFileUtils.createTempFile("testFileHandleLeakage");
MetadataUpload metaUp = new MetadataUpload(metadata, tmpFile);
connector.put(null, Arrays.asList(metaUp));
assertTrue("Leaking file handle in metadata upload", tmpFile.delete());
tmpFile = TestFileUtils.createTempFile("testFileHandleLeakage");
ArtifactDownload artDown = new ArtifactDownload(artifact, null, tmpFile, null);
connector.get(Arrays.asList(artDown), null);
new File(tmpFile.getAbsolutePath() + ".sha1").deleteOnExit();
assertTrue("Leaking file handle in artifact download", tmpFile.delete());
tmpFile = TestFileUtils.createTempFile("testFileHandleLeakage");
MetadataDownload metaDown = new MetadataDownload(metadata, null, tmpFile, null);
connector.get(null, Arrays.asList(metaDown));
new File(tmpFile.getAbsolutePath() + ".sha1").deleteOnExit();
assertTrue("Leaking file handle in metadata download", tmpFile.delete());
connector.close();
}
private static class CountingTransferListener extends AbstractTransferListener {
public final AtomicInteger successCount = new AtomicInteger();
@Override
public void transferSucceeded(TransferEvent event) {
successCount.incrementAndGet();
}
}
@Test
public void testBlocking() throws NoRepositoryConnectorException, IOException {
RepositoryConnector connector = connector();
int count = 10;
byte[] pattern = "tmpFile".getBytes("UTF-8");
File tmpFile = TestFileUtils.createTempFile(pattern, 100000);
CountingTransferListener artUpsCounter = new CountingTransferListener();
CountingTransferListener metaUpsCounter = new CountingTransferListener();
CountingTransferListener artDownsCounter = new CountingTransferListener();
CountingTransferListener metaDownsCounter = new CountingTransferListener();
List<ArtifactUpload> artUps =
createTransfers(ArtifactUpload.class, count, tmpFile, artUpsCounter);
List<MetadataUpload> metaUps =
createTransfers(MetadataUpload.class, count, tmpFile, metaUpsCounter);
List<ArtifactDownload> artDowns =
createTransfers(ArtifactDownload.class, count, null, artDownsCounter);
List<MetadataDownload> metaDowns =
createTransfers(MetadataDownload.class, count, null, metaDownsCounter);
// this should block until all transfers are done - racing condition, better way to test this?
connector.put(artUps, metaUps);
connector.get(artDowns, metaDowns);
Assert.assertEquals(count, artUpsCounter.successCount.intValue());
Assert.assertEquals(count, metaUpsCounter.successCount.intValue());
Assert.assertEquals(count, artDownsCounter.successCount.intValue());
Assert.assertEquals(count, metaDownsCounter.successCount.intValue());
connector.close();
}
public void testMkdirConcurrencyBug() throws IOException, NoRepositoryConnectorException {
RepositoryConnector connector = connector();
File artifactFile = TestFileUtils.createTempFile("mkdirsBug0");
File metadataFile = TestFileUtils.createTempFile("mkdirsBug1");
int numTransfers = 2;
ArtifactUpload[] artUps = new ArtifactUpload[numTransfers];
MetadataUpload[] metaUps = new MetadataUpload[numTransfers];
for (int i = 0; i < numTransfers; i++) {
Artifact art = new DefaultArtifact("testGroup", "testArtifact", "", "jar", i + "-test");
Metadata meta = new DefaultMetadata("testGroup", "testArtifact", i + "-test", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
ArtifactUpload artUp = new ArtifactUpload(art, artifactFile);
MetadataUpload metaUp = new MetadataUpload(meta, metadataFile);
artUps[i] = artUp;
metaUps[i] = metaUp;
}
connector.put(Arrays.asList(artUps), null);
connector.put(null, Arrays.asList(metaUps));
File localRepo = session().getLocalRepository().getBasedir();
StringBuilder localPath = new StringBuilder(localRepo.getAbsolutePath());
for (int i = 0; i < 50; i++) {
localPath.append("/d");
}
ArtifactDownload[] artDowns = new ArtifactDownload[numTransfers];
MetadataDownload[] metaDowns = new MetadataDownload[numTransfers];
for (int m = 0; m < 20; m++) {
CountingTransferListener artDownsCounter = new CountingTransferListener();
CountingTransferListener metaDownsCounter = new CountingTransferListener();
for (int i = 0; i < numTransfers; i++) {
File artFile = new File(localPath.toString() + "/a" + i);
File metaFile = new File(localPath.toString() + "/m" + i);
Artifact art = new DefaultArtifact("testGroup", "testArtifact", "", "jar", i + "-test");
Metadata meta = new DefaultMetadata("testGroup", "testArtifact", i + "-test", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
ArtifactDownload artDown = new ArtifactDownload(art, null, artFile, RepositoryPolicy.CHECKSUM_POLICY_FAIL);
artDown.setListener(artDownsCounter);
MetadataDownload metaDown = new MetadataDownload(meta, null, metaFile, RepositoryPolicy.CHECKSUM_POLICY_FAIL);
metaDown.setListener(metaDownsCounter);
artDowns[i] = artDown;
metaDowns[i] = metaDown;
}
connector.get(Arrays.asList(artDowns), Arrays.asList(metaDowns));
Assert.assertEquals(numTransfers, artDownsCounter.successCount.intValue());
Assert.assertEquals(numTransfers, metaDownsCounter.successCount.intValue());
TestFileUtils.deleteFile(localRepo);
}
connector.close();
}
/**
* See https://issues.sonatype.org/browse/AETHER-8
*/
public void testTransferZeroBytesFile() throws IOException, NoRepositoryConnectorException {
File emptyFile = TestFileUtils.createTempFile("");
Artifact artifact = new DefaultArtifact("gid:aid:ext:ver");
ArtifactUpload upA = new ArtifactUpload(artifact, emptyFile);
File dir = TestFileUtils.createTempDir("con-test");
File downAFile = new File(dir, "downA.file");
downAFile.deleteOnExit();
ArtifactDownload downA = new ArtifactDownload(artifact, "", downAFile, RepositoryPolicy.CHECKSUM_POLICY_FAIL);
Metadata metadata = new DefaultMetadata("gid", "aid", "ver", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
MetadataUpload upM = new MetadataUpload(metadata, emptyFile);
File downMFile = new File(dir, "downM.file");
downMFile.deleteOnExit();
MetadataDownload downM = new MetadataDownload(metadata, "", downMFile, RepositoryPolicy.CHECKSUM_POLICY_FAIL);
RepositoryConnector connector = connector();
connector.put(Arrays.asList(upA), Arrays.asList(upM));
connector.get(Arrays.asList(downA), Arrays.asList(downM));
assertNull(String.valueOf(upA.getException()), upA.getException());
assertNull(String.valueOf(upM.getException()), upM.getException());
assertNull(String.valueOf(downA.getException()), downA.getException());
assertNull(String.valueOf(downM.getException()), downM.getException());
assertEquals(0, downAFile.length());
assertEquals(0, downMFile.length());
connector.close();
}
public void testProgressEventsDataBuffer() throws UnsupportedEncodingException, IOException, NoSuchAlgorithmException, NoRepositoryConnectorException {
byte[] bytes = "These are the test contents.\n".getBytes("UTF-8");
int count = 120000;
MessageDigest digest = MessageDigest.getInstance("SHA-1");
for (int i = 0; i < count; i++) {
digest.update(bytes);
}
byte[] hash = digest.digest();
File file = TestFileUtils.createTempFile(bytes, count);
Artifact artifact = new DefaultArtifact("gid:aid:ext:ver");
ArtifactUpload upA = new ArtifactUpload(artifact, file);
File dir = TestFileUtils.createTempDir("con-test");
File downAFile = new File(dir, "downA.file");
downAFile.deleteOnExit();
ArtifactDownload downA = new ArtifactDownload(artifact, "", downAFile, RepositoryPolicy.CHECKSUM_POLICY_FAIL);
Metadata metadata = new DefaultMetadata("gid", "aid", "ver", "maven-metadata.xml", Metadata.Nature.RELEASE_OR_SNAPSHOT);
MetadataUpload upM = new MetadataUpload(metadata, file);
File downMFile = new File(dir, "downM.file");
downMFile.deleteOnExit();
MetadataDownload downM = new MetadataDownload(metadata, "", downMFile, RepositoryPolicy.CHECKSUM_POLICY_FAIL);
DigestingTransferListener listener = new DigestingTransferListener();
upA.setListener(listener);
upM.setListener(listener);
downA.setListener(listener);
downM.setListener(listener);
RepositoryConnector connector = connector();
connector.put(Arrays.asList(upA), null);
assertArrayEquals(hash, listener.getHash());
listener.rewind();
connector.put(null, Arrays.asList(upM));
assertArrayEquals(hash, listener.getHash());
listener.rewind();
connector.get(Arrays.asList(downA), null);
assertArrayEquals(hash, listener.getHash());
listener.rewind();
connector.get(null, Arrays.asList(downM));
assertArrayEquals(hash, listener.getHash());
listener.rewind();
connector.close();
}
private final class DigestingTransferListener implements TransferListener {
private MessageDigest digest;
private synchronized void initDigest() throws NoSuchAlgorithmException {
digest = MessageDigest.getInstance("SHA-1");
}
public DigestingTransferListener() throws NoSuchAlgorithmException {
initDigest();
}
public void rewind() throws NoSuchAlgorithmException {
initDigest();
}
public void transferSucceeded(TransferEvent event) {
}
public void transferStarted(TransferEvent event) throws TransferCancelledException {
}
public synchronized void transferProgressed(TransferEvent event) throws TransferCancelledException {
digest.update(event.getDataBuffer());
}
public void transferInitiated(TransferEvent event) throws TransferCancelledException {
}
public void transferFailed(TransferEvent event) {
}
public void transferCorrupted(TransferEvent event) throws TransferCancelledException {
}
public synchronized byte[] getHash() {
return digest.digest();
}
}
//
// Events testing
//
/**
* Test the order of events and their properties for the successful up- and download of artifact and metadata.
*/
public static void testSuccessfulTransferEvents(RepositoryConnectorFactory factory, DefaultRepositorySystemSession session, RemoteRepository repository) throws NoRepositoryConnectorException,
IOException {
RecordingTransferListener listener = new RecordingTransferListener(session.getTransferListener());
RepositoryConnector connector = factory.newInstance(session, repository);
byte[] pattern = "tmpFile".getBytes();
File tmpFile = TestFileUtils.createTempFile(pattern, 10000);
long expectedBytes = tmpFile.length();
Collection<ArtifactUpload> artUps = createTransfers(ArtifactUpload.class, 1, tmpFile, listener);
Collection<ArtifactDownload> artDowns = createTransfers(ArtifactDownload.class, 1, tmpFile, listener);
Collection<MetadataUpload> metaUps = createTransfers(MetadataUpload.class, 1, tmpFile, listener);
Collection<MetadataDownload> metaDowns = createTransfers(MetadataDownload.class, 1, tmpFile, listener);
connector.put(artUps, null);
LinkedList<TransferEvent> events = new LinkedList<TransferEvent>(listener.getEvents());
checkEvents(events, expectedBytes);
listener.clear();
connector.get(artDowns, null);
events = new LinkedList<TransferEvent>(listener.getEvents());
checkEvents(events, expectedBytes);
listener.clear();
connector.put(null, metaUps);
events = new LinkedList<TransferEvent>(listener.getEvents());
checkEvents(events, expectedBytes);
listener.clear();
connector.get(null, metaDowns);
events = new LinkedList<TransferEvent>(listener.getEvents());
checkEvents(events, expectedBytes);
connector.close();
session.setTransferListener(null);
}
private static void checkEvents(Queue<TransferEvent> events, long expectedBytes) {
TransferEvent currentEvent = events.poll();
String msg = "initiate event is missing";
assertNotNull(msg, currentEvent);
assertEquals(msg, INITIATED, currentEvent.getType());
checkProperties(currentEvent);
TransferResource expectedResource = currentEvent.getResource();
currentEvent = events.poll();
msg = "start event is missing";
assertNotNull(msg, currentEvent);
assertEquals(msg, TransferEvent.EventType.STARTED, currentEvent.getType());
assertEquals("bad content length", expectedBytes, currentEvent.getResource().getContentLength());
checkProperties(currentEvent);
assertResourceEquals(expectedResource, currentEvent.getResource());
EventType progressed = TransferEvent.EventType.PROGRESSED;
EventType succeeded = TransferEvent.EventType.SUCCEEDED;
TransferEvent succeedEvent = null;
int dataLength = 0;
long transferredBytes = 0;
while ((currentEvent = events.poll()) != null) {
EventType currentType = currentEvent.getType();
assertResourceEquals(expectedResource, currentEvent.getResource());
if (succeeded.equals(currentType)) {
succeedEvent = currentEvent;
checkProperties(currentEvent);
break;
} else {
assertTrue("event is not 'succeeded' and not 'progressed'", progressed.equals(currentType));
assertTrue("wrong order of progressed events, transferredSize got smaller, last = " + transferredBytes + ", current = " + currentEvent.getTransferredBytes(),
currentEvent.getTransferredBytes() >= transferredBytes);
assertEquals("bad content length", expectedBytes, currentEvent.getResource().getContentLength());
transferredBytes = currentEvent.getTransferredBytes();
dataLength += currentEvent.getDataBuffer().remaining();
checkProperties(currentEvent);
}
}
// all events consumed
assertEquals("too many events left: " + events.toString(), 0, events.size());
// test transferred size
assertEquals("progress events transferred bytes don't match: data length does not add up", expectedBytes, dataLength);
assertEquals("succeed event transferred bytes don't match", expectedBytes, succeedEvent.getTransferredBytes());
}
private static void assertResourceEquals(TransferResource expected, TransferResource actual) {
assertEquals("TransferResource: content length does not match.", expected.getContentLength(), actual.getContentLength());
assertEquals("TransferResource: file does not match.", expected.getFile(), actual.getFile());
assertEquals("TransferResource: repo url does not match.", expected.getRepositoryUrl(), actual.getRepositoryUrl());
assertEquals("TransferResource: transfer start time does not match.", expected.getTransferStartTime(), actual.getTransferStartTime());
assertEquals("TransferResource: name does not match.", expected.getResourceName(), actual.getResourceName());
}
private static void checkProperties(TransferEvent event) {
assertNotNull("resource is null for type: " + event.getType(), event.getResource());
assertNotNull("request type is null for type: " + event.getType(), event.getRequestType());
assertNotNull("type is null for type: " + event.getType(), event.getType());
if (PROGRESSED.equals(event.getType())) {
assertNotNull("data buffer is null for type: " + event.getType(), event.getDataBuffer());
assertTrue("transferred byte is not set/not positive for type: " + event.getType(), event.getTransferredBytes() > -1);
} else if (SUCCEEDED.equals(event.getType())) {
assertTrue("transferred byte is not set/not positive for type: " + event.getType(), event.getTransferredBytes() > -1);
}
}
/**
* Test the order of events and their properties for the unsuccessful up- and download of artifact and metadata.
* Failure is triggered by setting the file to transfer to {@code null} for uploads, and asking for a non-existent
* item for downloads.
*/
public static void testFailedTransferEvents(RepositoryConnectorFactory factory, DefaultRepositorySystemSession session, RemoteRepository repository) throws NoRepositoryConnectorException, IOException {
RecordingTransferListener listener = new RecordingTransferListener(session.getTransferListener());
RepositoryConnector connector = factory.newInstance(session, repository);
byte[] pattern = "tmpFile".getBytes("us-ascii");
File tmpFile = TestFileUtils.createTempFile(pattern, 10000);
Collection<ArtifactUpload> artUps = createTransfers(ArtifactUpload.class, 1, null, listener);
Collection<ArtifactDownload> artDowns = createTransfers(ArtifactDownload.class, 1, tmpFile, listener);
Collection<MetadataUpload> metaUps = createTransfers(MetadataUpload.class, 1, null, listener);
Collection<MetadataDownload> metaDowns = createTransfers(MetadataDownload.class, 1, tmpFile, listener);
connector.put(artUps, null);
LinkedList<TransferEvent> events = new LinkedList<TransferEvent>(listener.getEvents());
checkFailedEvents(events, null);
listener.clear();
connector.get(artDowns, null);
events = new LinkedList<TransferEvent>(listener.getEvents());
checkFailedEvents(events, null);
listener.clear();
connector.put(null, metaUps);
events = new LinkedList<TransferEvent>(listener.getEvents());
checkFailedEvents(events, null);
listener.clear();
connector.get(null, metaDowns);
events = new LinkedList<TransferEvent>(listener.getEvents());
checkFailedEvents(events, null);
connector.close();
session.setTransferListener(null);
}
private static void checkFailedEvents(Queue<TransferEvent> events, Class<? extends Throwable> expectedError) {
if (expectedError == null) {
expectedError = Throwable.class;
}
TransferEvent currentEvent = events.poll();
String msg = "initiate event is missing";
assertNotNull(msg, currentEvent);
assertEquals(msg, INITIATED, currentEvent.getType());
checkProperties(currentEvent);
currentEvent = events.poll();
msg = "fail event is missing";
assertNotNull(msg, currentEvent);
assertEquals(msg, TransferEvent.EventType.FAILED, currentEvent.getType());
checkProperties(currentEvent);
assertNotNull("exception is missing for fail event", currentEvent.getException());
Exception exception = currentEvent.getException();
assertTrue("exception is of wrong type, should be instance of " + expectedError + " but was " + exception.getClass(), expectedError.isAssignableFrom(exception.getClass()));
// all events consumed
assertEquals("too many events left: " + events.toString(), 0, events.size());
}
//
// Utils
//
/**
* Creates transfer objects according to the given class. If the file parameter is {@code null}, a new temporary
* file will be created for downloads. Uploads will just use the parameter as it is.
*/
public static <T extends Transfer> List<T> createTransfers(Class<T> cls, int count, File file, TransferListener listener) {
ArrayList<T> ret = new ArrayList<T>();
for (int i = 0; i < count; i++) {
String context = null;
String checksumPolicy = RepositoryPolicy.CHECKSUM_POLICY_IGNORE;
Object obj = null;
if (cls.isAssignableFrom(ArtifactUpload.class)) {
Artifact artifact = new DefaultArtifact("testGroup", "testArtifact", "sources", "jar", (i + 1) + "-test");
ArtifactUpload artifactUpload = new ArtifactUpload(artifact, file);
artifactUpload.setListener(listener);
obj = artifactUpload;
} else if (cls.isAssignableFrom(ArtifactDownload.class)) {
try {
Artifact artifact = new DefaultArtifact("testGroup", "testArtifact", "sources", "jar", (i + 1) + "-test");
ArtifactDownload artifactDownload = new ArtifactDownload(artifact, context, safeFile(file), checksumPolicy);
artifactDownload.setListener(listener);
obj = artifactDownload;
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
} else if (cls.isAssignableFrom(MetadataUpload.class)) {
Metadata metadata = new DefaultMetadata("testGroup", "testArtifact", (i + 1) + "-test", "jar", Metadata.Nature.RELEASE_OR_SNAPSHOT, file);
MetadataUpload metadataUpload = new MetadataUpload(metadata, file);
metadataUpload.setListener(listener);
obj = metadataUpload;
} else if (cls.isAssignableFrom(MetadataDownload.class)) {
try {
Metadata metadata = new DefaultMetadata("testGroup", "testArtifact", (i + 1) + "-test", "jar", Metadata.Nature.RELEASE_OR_SNAPSHOT, file);
MetadataDownload metadataDownload = new MetadataDownload(metadata, context, safeFile(file), checksumPolicy);
metadataDownload.setListener(listener);
obj = metadataDownload;
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
ret.add(cls.cast(obj));
}
return ret;
}
public static <T extends Transfer> List<T> createTransfers(Class<T> cls, int count, File file) {
return createTransfers(cls, count, file, null);
}
private static File safeFile(File file) throws IOException {
return file == null ? TestFileUtils.createTempFile("") : file;
}
}